{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Simple polarimeter\n",
    "\n",
    "We will simulate a simple polarimeter that measures linear polarization states. In this example we will use this polarimeter to measure the polarization state of starlight. \n",
    "\n",
    "In optical and near-infrared wavelengths it is with the current technology not possible to directly measure the electric field of starlight, only its intensity. Therefore, it is also not possible to directly measure its polarization state. However, this is a property of light that we would like to measure as it can contain important astrophysical information, e.g. on dust grain sizes and magnetic fields. To measure the polarization state we will have to encode this information into intensity measurements that, in post-processing, can be combined into an estimate of the polarization state. \n",
    "\n",
    "We assume that the reader is familiar with basic polarization concepts, i.e. Stokes vectors, Mueller matrices, waveplates and polarizers. A good introduction on polarimetry can be found in Snik, F., & Keller, C. U. (2013). Astronomical polarimetry: polarized views of stars and planets.\n",
    "\n",
    "In this tutorial we will use polarization optics that modulate in time to get us intensity measurements that contain polarization information. We will use a static linear polarizer, an component that only transmits one linear polarization state (e.g. horizontal polarized light), as analyzer. Before the polarizer we will put a half-wave plate (HWP). The HWP will rotate and act as the polarization modulator, which means that it transforms the linear polarization states (horizontal, vertical, +45 degree and -45 degrees) to the polarization state transmitted by the polarizer. To do so, the HWP will cycle between 0, 45, 22.5 and 67.5 degrees of rotation. For every HWP position we will measure the intensity. We can put these four intensity measurements into a vector. Multiplying this vector with the demodulation matrix results into the Stokes vector. The demodulation matrix describes how the intensity measurements encode polarization information and is based on the architecture of the polarization modulation. \n",
    "\n",
    "As this polarimeter uses a HWP as polarization modulator, it can not transform circular polarization into the linear polarization state that the polarizer transmits. Thus this polarimeter is insenstive for circular polarization. We will see this at the end of the tutorial. \n",
    "\n",
    "We start by importing the relevant python modules. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt \n",
    "\n",
    "from hcipy import * "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We will do our measurements with a 4 meter diameter telescope at a wavelength of 500 nanometer. This will set the spatial resolution of the system. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# parameters telescope\n",
    "telescope_diameter = 4 # meter \n",
    "wavelength = 500E-9 #meter\n",
    "\n",
    "# the spatial resolution\n",
    "spatial_resolution_telescope = wavelength / telescope_diameter"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This allows us to set the pupil- and focal-plane grids, and the propagator."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# setting the grids\n",
    "pupil_grid = make_pupil_grid(128, telescope_diameter)\n",
    "focal_grid = make_focal_grid(q=6, num_airy=10, spatial_resolution=spatial_resolution_telescope)\n",
    "\n",
    "# the propagator between the pupil- and focal-grid. \n",
    "propagator = FraunhoferPropagator(pupil_grid, focal_grid) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Defining the aperture, the HWP and its positions, the linear polarizer and the detector. This detector is perfect in the sense that it has no dark current, no read noise and no flat field errors. Therefore, we will only suffer from photon noise. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "aperture = circular_aperture(telescope_diameter)(pupil_grid)\n",
    "\n",
    "HWP = HalfWavePlate(0)\n",
    "\n",
    "HWP_positions = [0, 45, 22.5, 67.5] # degrees\n",
    "\n",
    "polarizer = LinearPolarizer(0)\n",
    "\n",
    "detector = NoisyDetector(focal_grid, dark_current_rate=0, read_noise=0, flat_field=0, include_photon_noise=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We now define the parameters for the starlight, e.g. its flux level and polarization state, which we eventually hope to measure."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "# parameters star\n",
    "zero_magnitude_flux = 3.9E10 # photons / sec\n",
    "stellar_magnitude = 8\n",
    "\n",
    "# the polarization state of the starlight.\n",
    "stokes_vector_star = np.array([1,0.5,-0.01,0.05])\n",
    "\n",
    "# Here we give the wavefront the properties (power, polarization state) of the starlight.\n",
    "pupil_wavefront = Wavefront(aperture, wavelength, input_stokes_vector=stokes_vector_star)\n",
    "\n",
    "pupil_wavefront.total_power = zero_magnitude_flux * 10**(-stellar_magnitude / 2.5)\n",
    "\n",
    "print(\"Total photon flux {:g} photons / sec.\".format(pupil_wavefront.total_power))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can check the polarization state of the wavefront simply by: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stokes_parameters = [pupil_wavefront.I, pupil_wavefront.Q, pupil_wavefront.U, pupil_wavefront.V]\n",
    "titles = ['I', 'Q', 'U', 'V']\n",
    "\n",
    "# The value that we use to normalize the stokes vector with. \n",
    "max_val = np.max(stokes_parameters[0])\n",
    "\n",
    "k=1\n",
    "plt.figure(figsize=(12, 8))\n",
    "\n",
    "for stokes_parameter, title in zip(stokes_parameters, titles):\n",
    "        \n",
    "    if max_val == 0:\n",
    "        max_val = 1\n",
    "\n",
    "    plt.subplot(2,2,k)\n",
    "    \n",
    "    if title == 'I':\n",
    "        cmap = 'inferno'\n",
    "        vmin = 0\n",
    "        vmax = 1\n",
    "    else:\n",
    "        cmap = 'bwr'\n",
    "        vmin = -1\n",
    "        vmax = 1\n",
    "        \n",
    "    if title == 'U' or title == 'V':\n",
    "        plt.xlabel('x [meter]')\n",
    "    \n",
    "    if title == 'I' or title == 'U':\n",
    "        plt.ylabel('y [meter]')\n",
    "\n",
    "    imshow_field(stokes_parameter / max_val, cmap=cmap, vmin=vmin, vmax=vmax)\n",
    "    \n",
    "    plt.title('Stokes ' + title)\n",
    "    k += 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We will now simulate our polarimeter for a given time duration. During the simulation we will perform multiple HWP cycles. During one HWP cycle the HWP will rotate through its four positions. For every HWP position we will make an intensity measurement.   "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# total duration of the measurement\n",
    "measurement_duration = 8 # seconds\n",
    "\n",
    "# number of times we go through a HWP cycle\n",
    "HWP_cycles = 4\n",
    "\n",
    "# integration time per measurement\n",
    "delta_t = measurement_duration / (HWP_cycles * 4) \n",
    "\n",
    "# counter for the state of the modulation loop \n",
    "k = 0 \n",
    "\n",
    "# The arrays where the measurements are saved \n",
    "measurements = Field(np.zeros((4, focal_grid.size)), focal_grid)\n",
    "\n",
    "# looping through the time steps \n",
    "for t in np.linspace(0, measurement_duration, HWP_cycles * 4):\n",
    "    \n",
    "    # rotating the HWP to its new position\n",
    "    HWP.fast_axis_orientation = np.radians(HWP_positions[k])\n",
    "        \n",
    "    # we propagate the wavefront through the half-wave plate \n",
    "    pupil_wavefront_2 = HWP.forward(pupil_wavefront)\n",
    "    \n",
    "    # we propagate the wavefront through the linear polarizer \n",
    "    pupil_wavefront_3 = polarizer.forward(pupil_wavefront_2)\n",
    "    \n",
    "    focal_wavefront = propagator(pupil_wavefront_3)\n",
    "\n",
    "    detector.integrate(focal_wavefront, dt=delta_t)\n",
    "    \n",
    "    # reading out the detector in the correct element of the measurement array\n",
    "    measurements[k,:] += detector.read_out()\n",
    "    \n",
    "    # Moving to the next HWP position\n",
    "    k += 1 \n",
    "\n",
    "    # resetting the HWP to its intial position\n",
    "    if k > 3:\n",
    "        k = 0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lets plot the measurements for the various HWP positions and the total number of photons. Note that the measurements have different numbers of photons, this is due to the starlight's polarization state. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(12, 8))\n",
    "\n",
    "max_val_meas = np.max(measurements) \n",
    "\n",
    "for i in np.arange(4):\n",
    "    plt.subplot(2,2,i+1)\n",
    "    imshow_field(measurements[i,:], vmin=0, vmax=max_val_meas)\n",
    "    \n",
    "    if i > 1:\n",
    "        plt.xlabel('x [rad]')\n",
    "    \n",
    "    if i == 0 or i == 2:\n",
    "        plt.ylabel('y [rad]')\n",
    "    \n",
    "    print('\\nHWP position ', i+1)\n",
    "    print('Number of photons = ', int(np.sum(measurements[i,:])))\n",
    "\n",
    "    plt.title('HWP position ' + str(i+1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We now have our measurements, which we want to convert into a polarization state. We do this by multiplying the measurements with a demodulation matrix. This matrix combines the measurements in such a way that the polarization state is retrieved. The demodulation matrix for this system is given by:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# defining the demodulation matrix\n",
    "demodulation_matrix = np.zeros((4,4))\n",
    "\n",
    "# demodulation for I\n",
    "demodulation_matrix[0,:] = 0.25\n",
    "\n",
    "# demodulation for Q\n",
    "demodulation_matrix[1,0] = 0.5\n",
    "demodulation_matrix[1,1] = -0.5\n",
    "\n",
    "# demodulation for U\n",
    "demodulation_matrix[2,2] = 0.5\n",
    "demodulation_matrix[2,3] = -0.5\n",
    "\n",
    "print('demodulation matrix = \\n', demodulation_matrix)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's do aperture photometry on the star to construct a 1-dimensional measurement vector. We use an aperture with a diameter of the spatial resolution of the telescope (i.e. $1$ $\\lambda/D$). \n",
    "\n",
    "After that, we calculate the polarization state by multiplying this vector with the demodulation matrix."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "photometry_aperture = circular_aperture(spatial_resolution_telescope)(focal_grid)\n",
    "\n",
    "# generating the measurement vector by doing aperture photometry. \n",
    "measurement_vector = np.array(np.sum(measurements[:,photometry_aperture==1], axis=1))\n",
    "\n",
    "# calculating the measured Stokes vector \n",
    "stokes_measured = field_dot(demodulation_matrix, measurement_vector)\n",
    "\n",
    "print('Measured Stokes vector = \\n', stokes_measured / stokes_measured[0])\n",
    "\n",
    "print('Input Stokes vector = \\n', pupil_wavefront.input_stokes_vector)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We see that we can completely retrieve the linear polarization state, but that we are not able to measure circular polarization.\n",
    "\n",
    "In this tutorial we have shown how to simulate a simple, and ideal polarimeter with hcipy. We have implemented a rotating HWP and linear polarizer, reconstructed the measured polarization state, and have seen that it can only measure linear polarization states. We have only discussed an ideal system without any noise source except photon noise. Using hcipy it is very easy to make these simulations more realistic by adding for example non-perfect polarization optics, atmospheric turbulence and adaptive optics, and detector noise (e.g. dark current, read noise, flat field effects). It is also possible to simulate polarimeters that measure all Stokes vector elements. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.2"
  },
  "level": "intermediate",
  "thumbnail_figure": -1
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
